/*--------------------------------------------------------
Copyright (c) 2010 Apolo Pena

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------*/
package org.flashalchemy.ezlib.ezlog
{
    import org.flashalchemy.ezlib.ezlog.events.EZLogDataXmlParserEvent;
    import org.flashalchemy.ezlib.ezlog.data.EzLogData;
    import org.flashalchemy.ezlib.ezlog.data.EzLogDataValidator;
    import org.flashalchemy.ezlib.ezlog.data.EzLogDataXmlParser;
    import org.flashalchemy.ezlib.ezlog.targets.EzProgramConsoleTarget;
    import org.flashalchemy.ezlib.ezlog.targets.EzTraceTarget;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.utils.getQualifiedClassName;
    import mx.logging.ILogger;
    import mx.logging.ILoggingTarget;
    import mx.logging.Log;


    /**
     * @inherit
     **/ 
    /**
     *
     * <b>Description: </b>EzLog is a Singleton class providing Singleton access to a dynamically configured ILogger object
     * which an application and its various components can utilize for logging/debugging purposes.<br/>
     * EzLog should be initialized with a formatted target of type <code>ILoggingTarget</code> so that all EzLog.getLogger() calls
     * will be made with a standard formatted target rather than the Flex default.<br/>The flexlogger Standard is to initialize the
     * EzLog Singleton from the preinitialize event the main application .mxml file using an ILoggingTarget of type <code>org.flashalchemy.ezlib.logging.targets.EzTraceTarget</code>
     * <p/><i>The first line of code that runs in your main application .mxml files should be the initialization of the EzLog.</i> That line of code should look something like this:<p/>
     * <code>EzLog.getInstance().initialize(new EzTraceTarget());</code><p/>
     * <i>Typically the second line of code that runs in your main application .mxml file is assigning a logger retrieved from the Log to a class property so that your
     * main application can make calls to the Log and receive events from the Log.</i><p/>
     * Example: <code> logger = EzLog.getLogger(this); </code><p/>
     *
     * <b>Usage: </b>EzLog cannot be directly instantiated since it is a class that employs the Singleton design pattern.
     * EzLog should be initialized once per application session via the <code>EzLog.getInstance().initialize(logTarget:ILoggingTarget)</code> public method. All subsequent calls to
     * <code>EzLog.getInstance().initialize(logTarget:ILoggingTarget)</code> will send and errorneous trace message to the console and no further code will run in that method.</p>
     * Once initialized, EzLog should be used via its static API which is described in detail in the method section.
     *
     */
    public final class EzLog extends EventDispatcher
    {

        /**
         * @private
         * flag so that we know if a filter array has ever been registered with this class
         */
        private static var _isOriginalFiltersRegistered:Boolean = false;

        /**
         * @private
         * Initialization flag
         *
         */
        private static var _initialized:Boolean = false;

        /**
         * @private
         * Initialization flag
         *
         */
        private static var _initializedViaXml:Boolean = false;

        /**
         * @private
         * error message 1
         **/
        private static var errorMessage1:String = 'The logger being returned from this method will use the last log target set by an outside entity (if applicable).';

        /**
         * @private
         * logger for error and warning regarding the EzLog it'self
         */
        private static var logger:ILogger;

        /**
         * Boolean variable indicating if this Singleton class (EzLog) has been initialized.
         * Remember that this class (EzLog) should only be initialized once. Somtimes you will use
         * this variable to make a check to see if it is safe to initialize this class.
         **/
        public static function get isInitialized():Boolean
        {
            return _initialized;
        }

        /**
         * The filters used when the EzLog was initialized
         * @return
         */
        public static function get originalFilters():Array
        {
            return _originalFilters;
        }

        /**
         * @private
         * internal reference to the first filter registered with this class. If no filter has ever been registered then value of the variable will be an empty array.
         */
        private static var _originalFilters:Array = [];

        /**
         * @private
         * internal reference to the current filter registered with this class
         *
         */
        private static var _lastKnownFilters:Array = [];

        /**
         * reference to the current qualified filter array registered with this class.
         *
         */
        public static function get lastKnownFilters():Array
        {
            return _lastKnownFilters
        }

        /**
         * @private
         * Defines the singleton instance of the EzLog
         *
         */
        private static var _instance:EzLog;

        /**
         * @private
         * internal reference to the current log target
         *
         */
        private static var _logTarget:ILoggingTarget;

        /**
         * @private
         * internal reference to the default trace target
         *
         */
        private static var _originalLogTarget:ILoggingTarget;

        //--------------------------------------------------------------------------
        //
        //  Constructor
        //
        //--------------------------------------------------------------------------
        /**
         * This is (simulated) Private constructor, do not directly instantiate this class or an error will be thrown.
         *
         * @throws A Generic Error with the message "SINGLETON_EXCEPTION" if any class outside this one tries to instantiate it.
         */
        public function EzLog(access:Private)
        {
            super(this);
            if (access == null)
            {
                throw new Error("SINGLETON_EXCEPTION");
            }
            _instance = this;
        }

        //--------------------------------------------------------------------------
        //
        //  Methods
        //
        //--------------------------------------------------------------------------
        /**
         * @private
         * Convertes a qualified classname to a category string that can be used to register an object in the Log
         */
        private static function formatPackagePath(value:*):String
        {
            var qcn:String = getQualifiedClassName(value);
            // handle internal classes using '$'
            qcn = qcn.replace("$", ".");
            // handle public class in a package using '::'
            return qcn.replace("::", ".");
        }

        /**
         * Determines if the singleton instance of EzLog
         * has been instantiated.
         *
         * @return singleton instance of EzLog
         */
        public static function getInstance():EzLog
        {
            if (_instance == null)
            {
                _instance = new EzLog(new Private());
            }
            _originalLogTarget = new EzTraceTarget();
            Log.flush();
            return _instance;
        }

        /**
         * Initializes the Log by flushing the Log and adding the <code>$logTarget</code> to the Log.<p/>
         * <i>Note:</i> This method can only be called once, each subsequent call to this method will result in warning message to the IDE console.
         * No other code will execute in the method at that point.
         *
         * @param A formatted logging target (aka a 'trace target')
         */
        public function initialize($logTarget:ILoggingTarget):void
        {
            if (!_initialized)
            {
                if ($logTarget)
                {
                    logger = Log.getLogger("org.flashalchemy.ezlib.ezlog.EzLog");
                    _originalLogTarget = $logTarget;
                    _logTarget = $logTarget;
                    _originalFilters = _logTarget.filters;
                    _lastKnownFilters = _logTarget.filters;
                    Log.flush();
                    Log.addTarget(_originalLogTarget);
                }
                else
                {
                    EzLog.logger.error('initialize() error, null or undefined logTarget argument');
                }
                _initialized = true;
                _initializedViaXml = true;
            }
            else
            {
                EzLog.logger.warn('initialize(): This method can only be called 1 time and it has allready been called, no actions were taken.');
            }
        }
        /**
         * Initializes the Log by flushing the Log, loading and parsing a EzLogData .xml file and then configuring the EzLog based on the xml data
         * <i>Note:</i> This method can only be called once, each subsequent call to this method will result in a warning message to the IDE console.
         * No other code will execute in the method at that point.
         * <i>Note:</i> This method can only be called once, each subsequent call to this method will result in a warning message to the IDE console.
         * No other code will execute in the method at that point.
         *
         * @param The path to the EzLogData .xml file
         */
        public function initializeViaXml(xmlFilePath:String):void
        {
            var parser:EzLogDataXmlParser = new EzLogDataXmlParser();
            if (!_initialized && !_initializedViaXml)
            {
                logger = Log.getLogger("org.flashalchemy.ezlib.ezlog.EzLog");
                if (!xmlFilePath)
                {
                    EzLog.logger.error('initializeViaXml() error, null or undefined logTarget argument');
                }
                else
                {
                    parser.addEventListener(EZLogDataXmlParserEvent.PARSE_COMPLETE, onEzLogDataXmlParseComplete, false, 0, true);
                    parser.initialize((xmlFilePath));
                }
            }
        }

        private static var consoleTargets:Array = new Array();

        /**
         * @private
         * sets the prorper internal flags and dispatched a complete event
         * other entities should listen for this event because it is the indication
         * that EzLog is ready to
         **/
        private function onEzLogDataXmlParseComplete(event:EZLogDataXmlParserEvent):void
        {
            _initialized = true;
            _initializedViaXml = true;
            var logTargetLength:int = event.payload.length;
            var logTargets:Array = [];
            var ett:EzTraceTarget = new EzTraceTarget();
            var ettAdded:Boolean = false;
            var epct:EzProgramConsoleTarget;
            EzLog.flush();
            for each (var logData:EzLogData in event.payload)
            {
                switch (logData.logTargetData.targetType)
                {
                    case EzLogDataValidator.LOG_TARGET_TYPE_EZ_TRACE_TARGET:
                        switch (logData.logTargetData.formatting)
                    {
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_BLANK:
                            ett.omitLogInformation = true;
                            break;
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_DEFAULT:
                            // do nothing
                            break;
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_MINIMAL:
                            ett.includeTime = false;
                            ett.truncateLevel = true;
                            ett.prependMessageWithDelimiter = false;
                            break;
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_VERBOSE:
                            ett.includeDate = true;
                            ett.omitHours = false;
                            ett.omitPackagePathFromCategory = false;
                            break;
                        default:
                            logger.warn('invalid log target type');
                    }
                        if (!ettAdded)
                        {
                            ett.filters = logData.logTargetFilterData.filters;
                            EzLog.addTarget(ett);
                            ettAdded = true;
                        }
                        break;
                    case EzLogDataValidator.LOG_TARGET_TYPE_EZ_PROGRAM_CONSOLE_TARGET:
                        epct = new EzProgramConsoleTarget(logData.logTargetData.consoleTitle, logData.logTargetData.consoleAlpha, logData.logTargetData.consoleThemeId);
                        switch (logData.logTargetData.formatting)
                    {
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_BLANK:
                            epct.omitLogInformation = true;
                            break;
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_DEFAULT:
                            // do nothing
                            break;
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_MINIMAL:
                            epct.includeTime = false;
                            epct.truncateLevel = true;
                            epct.prependMessageWithDelimiter = false;
                            break;
                        case EzLogDataValidator.LOG_TARGET_FORMATTING_TYPE_VERBOSE:
                            epct.includeDate = true;
                            epct.omitHours = false;
                            epct.omitPackagePathFromCategory = false;
                            break;
                        default:
                            logger.warn('invalid filter format type');
                    }
                        epct.filters = logData.logTargetFilterData.filters;
                        EzLog.addTarget(epct);
                        consoleTargets.push(epct);
                        break;
                    default:
                        logger.warn('invalid log target type');
                }
            }
            dispatchEvent(new Event(Event.COMPLETE));
        }

        // TODO ensure these consoles are at the top of the application z-index
        /**
         * Cascades all console windows from the top left conrner, the state of each window will not be changed.
         * Should only be called when there is more than one console active in the program.
         */
        public static function cascadeConsoleWindows():void
        {
            for each (var target1:EzProgramConsoleTarget in consoleTargets)
            {
                target1.console.x = 0;
                target1.console.y = 0;
            }
            var count:int = 0;
            for each (var target2:EzProgramConsoleTarget in consoleTargets)
            {
                count++;
                target2.console.x += (40 * count);
                target2.console.y = (40 * count);
            }
        }

        // TODO ensure these consoles are at the top of the application z-index
        /**
         * Vertically arranges all console windows from the top left conrner, the state of each window will not be changed.
         * Should only be called when there is more than one console active in the program.
         */
        public static function arrangeConsoleWindowsVertically():void
        {
            for each (var target1:EzProgramConsoleTarget in consoleTargets)
            {
                target1.console.x = 0;
                target1.console.y = 0;
            }
            var count:int = 0;
            for each (var target2:EzProgramConsoleTarget in consoleTargets)
            {
                target2.console.y = (target2.console.height * count);
                count++;
            }
        }

        // TODO ensure these consoles are at the top of the application z-index
        /**
         * Horizontally arranges all console windows from the top left conrner, the state of each window will not be changed.
         * Should only be called when there is more than one console active in the program.
         */
        public static function arrangeConsoleWindowsHorizontally():void
        {
            for each (var target1:EzProgramConsoleTarget in consoleTargets)
            {
                target1.console.x = 0;
                target1.console.y = 0;
            }
            var count:int = 0;
            for each (var target2:EzProgramConsoleTarget in consoleTargets)
            {
                target2.console.x = ((target2.console.width + 16) * count);
                count++;
            }
        }

        /**
         * Takes a calling object argument and an ILogger retrieved from the Log using the category name of the calling object is returned.
         * The calling object must be passed in manually since the arguments.caller property was removed in Actionscript 3 :(
         *
         * @param callingObject A reference to the object calling this method.
         * @return a logger specifically for the calling object, use this logger to make calls to the Log.
         */
        public static function getLogger(callingObject:*):ILogger
        {
            if (!_initialized)
            {
                EzLog.logger.warn('getLogger() called before EzLog has been initialized. Ensure The EzLog is initialized from the preinitialize' + ' event of the main Application .mxml file');
            }
            var packagePath:String = formatPackagePath(callingObject);
            var logger:ILogger;
            try
            {
                logger = Log.getLogger(packagePath);
            }
            catch (e:Error)
            {
                EzLog.logger.error('getLogger() ERROR CAUGHT: Logger call was not processed. Unable to create logger instance using category path: ' + packagePath + '  true error=' + e.message);
            }
            return logger;
        }

        /**
         * Flushes the Log and resets the log target for the Log to be the original log target used when this class was initialized (see class description).
         * If this class has not been initialized then a <code>null</code> log target will be used.
         *
         * @param callingObject A reference to the object calling this method.
         * @return a ILogger instance that uses the logTarget that was passed in when This Singleton class was initialized (see class description).
         */
        public static function resetLogTarget(callingObject:*):ILogger
        {
            Log.flush();
            Log.addTarget(_originalLogTarget);
            return EzLog.getLogger(callingObject);
        }

        /**
         * Sets a filter on the current log target and returns a new instance of the Log. This method also validates the contents of the array passed in
         * and will abort with a trace warning if the array is not valid. Validation is implemented by using the mx.logging.Log.hasIllegalCharacters() API.<br/>
         * Use this method to swap filters without having to instantiate and swap to a new log target.
         *
         * @param callingObject A reference to the object calling this method.
         * @param qualifiedFilters An Array of legal category strings, this argument will be validated.
         * @return A new instance of the Logger that sets the passed in array filter used to the current log target.
         */
        public static function registerFilters(callingObject:*, qualifiedFilters:Array):ILogger
        {
            var filterTotal:int = qualifiedFilters.length;
            var isValidFilterArray:Boolean = true;
            var errorIndex:int = -1;
            if (filterTotal > 0)
            {
                for (var i:int = 0; i < filterTotal; i++)
                {
                    var isNotValid:Boolean = Log.hasIllegalCharacters(qualifiedFilters[i]);
                    if (isNotValid)
                    {
                        isValidFilterArray = false;
                        errorIndex = i;
                        break;
                    }
                }
                if (isValidFilterArray)
                {
                    var wasTrouble:Boolean = false;
                    try
                    {
                        _logTarget.filters = qualifiedFilters;
                    }
                    catch (e:Error)
                    {
                        wasTrouble = true;
                        EzLog.logger.warn('registerFilter() ERROR CAUGHT: unable to set log target filter. The filter was not set or registered. Ensure you have initialized EzLog ' + 'before calling this method. ' +
                                            errorMessage1 + ' Caught error message = ' + e.message);
                    }
                    if (!wasTrouble)
                    {
                        if (!_isOriginalFiltersRegistered)
                        {
                            _originalFilters = qualifiedFilters;
                            _isOriginalFiltersRegistered = true;
                        }
                        _lastKnownFilters = qualifiedFilters;
                    }
                }
                else
                {
                    EzLog.logger.warn('registerFilter() invalid filter array: The filters array passed in contained one or more invalid category strings, starting with index: ' + errorIndex + ', The filter was not set or registered. ' +
                                        EzLog.errorMessage1);
                }
            }
            else
            {
                EzLog.logger.warn('registerFilter() was passed in an empty array. ' + errorMessage1);
            }
            return EzLog.swapLogTarget(callingObject, _logTarget);
        }

        /**
         * Resets the filters on the current log target to the last filters used with that log target and returns a new instance of the Log.<br/>Use this method
         * to reinstate the last used filters array before the Log was flushed to the current log target.<br/> The public static methods of this class: swaplogTarget(), resetLogTarget(),
         * initialize(), and getInstance() all flush the Log when called which will clear out any custom filters being used at the moment.<br/>
         * This method is here so that the last used filter can be recovered if it is flushed away or changed by calling the various EzLog API's.
         *
         * @param callingObject A reference to the object calling this method.
         * @return A new instance of the Logger that sets the last known filter used to the current log target.
         */
        public static function swapToLastRegisteredFilters(callingObject:*):ILogger
        {
            return EzLog.swapLogTarget(callingObject, _logTarget);
        }

        /**
         * Resets the filters on the current log target to the original filters used when the EzLog was initialized and returns a new instance of the Log.<br/>Use this method
         * to reinstate the original filters array to the log target before the Log was flushed.<br/> The public static methods of this class: swaplogTarget(), resetLogTarget(),
         * initialize(), and getInstance() all flush the Log when called which will clear out any custom filters (or the original filter) being used at the moment.
         * This method is here so that the original filter used to initialize this class can be recovered if it is flushed or changed away by calling swaplogTarget(),
         * resetLogTarget(), initialize(), registerFilters() or getInstance().
         *
         * @param callingObject A reference to the object calling this method.
         * @return A new instance of the Logger that sets the original filter used (for the orginal log target that was used) to the current log target.
         */
        public static function swapToFirstRegisteredFilters(callingObject:*):ILogger
        {
            return EzLog.swapLogTarget(callingObject, _originalLogTarget);
        }

        /**
         * Flushes the Log then adds the <code>newLogTarget</code> to the Log and returns a new ILogger
         *
         * @param callingObject The calling object to be registered to the Log
         * @param newLogTarget The Log target to be adeded to the ILogger that is returned
         * @return a new ILogger registered to the callingObject (given the new log target is configured to do so)
         */
        public static function swapLogTarget(callingObject:*, newLogTarget:ILoggingTarget):ILogger
        {
            if (!callingObject)
            {
                EzLog.logger.warn('swapLogTarget(): null or undefined callingObject argument. The current unaltered Logger registered to the callingObject is being returned');
                return EzLog.getLogger(callingObject);
            }
            else if (!newLogTarget)
            {
                EzLog.logger.warn('swapLogTarget(): null or undefined newLogTarget argument. The current unaltered Logger registered to the callingObject is being returned');
                return EzLog.getLogger(callingObject);
            }
            _logTarget = newLogTarget
            Log.flush();
            Log.addTarget(_logTarget);
            return EzLog.getLogger(callingObject);
        }

        /**
         * Convert an array of classes or instances to an array of valid
         * category names.
         *
         * @param array of values to be converted to category strings
         * @return array of category strings

         */
        public static function getFilter(values:Array):Array
        {
            var categories:Array = [];
            for (var value:*in values)
            {
                value = values[value];
                if (value is Array)
                {
                    var qcn:String = getQualifiedClassName(value[0]);
                    value = qcn.slice(0, qcn.indexOf("::")).concat(".*");
                }
                categories.push((value is String) ? value : formatPackagePath(value));
            }
            return categories;
        }

        /**
         * Simple wrapper around the static method <code>Log.flush()</code>
         */
        public static function flush():void
        {
            Log.flush();
        }

        /**
         * Simple wrapper around the static method <code>Log.addTarget(target:ILogger)</code>. Use this method to add additional targets to the EzLog.
         */
        public static function addTarget(target:ILoggingTarget):void
        {
            Log.addTarget(target);
        }

        /**
         * Simple wrapper around the static method <code>Log.removeTarget(target:ILogger)</code>. Use this method to remove targets from the EzLog.
         */
        public static function removeTarget(target:ILoggingTarget):void
        {
            Log.removeTarget(target);
        }
    }
}


/**
 * @private
 * Inner class which restricts contructor access to Private, aka the 'lock'
 */
class Private
{
}

